/*
 * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */

package org.eclipse.imagen;

import java.awt.Point;
import java.awt.Rectangle;
import java.awt.image.Raster;
import java.awt.image.RenderedImage;
import java.awt.image.WritableRaster;
import java.util.Map;
import java.util.Vector;

/**
 * A general class for single-source operations which require cobbled sources and create an image consisting of a single
 * tile equal in location and size to the image bounds.
 *
 * <p>The output image will have a single tile, regardless of the <code>ImageLayout</code> settings passed to the
 * constructor. Any specified settings for tile grid offset and tile dimensions will be replaced by the image origin and
 * tile dimensions, respectively.
 *
 * <p>Subclasses should implement the <code>computeImage</code> method which requests computation of the entire image at
 * once.
 *
 * @see OpImage
 * @see org.eclipse.imagen.operator.DCTDescriptor
 * @see org.eclipse.imagen.operator.DFTDescriptor
 * @see org.eclipse.imagen.operator.ErrorDiffusionDescriptor
 */
public abstract class UntiledOpImage extends OpImage {
    /**
     * Creates the <code>ImageLayout</code> for the image. If the layout parameter is null, create a new <code>
     * ImageLayout</code> from the supplied <code>RenderedImage</code>. Also, force the tile grid offset to equal the
     * image origin and the tile width and height to be equal to the image width and height, respectively, thereby
     * forcing the image to have a single tile.
     *
     * @param layout The <code>ImageLayout</code> to be cloned; may be null.
     * @param source The <code>RenderedImage</code> the attributes of which are to be used as fallbacks in creating a
     *     new <code>ImageLayout</code>.
     * @return The <code>ImageLayout</code> to be used.
     */
    private static ImageLayout layoutHelper(ImageLayout layout, Vector sources) {
        if (sources.size() < 1) {
            throw new IllegalArgumentException(JaiI18N.getString("Generic5"));
        }

        RenderedImage source = (RenderedImage) sources.get(0);

        ImageLayout il = layout == null ? new ImageLayout() : (ImageLayout) layout.clone();

        // Force the image to have one tile. For this to obtain with minimal
        // tile size the tile grid offset must coincide with the image origin.
        il.setTileGridXOffset(il.getMinX(source));
        il.setTileGridYOffset(il.getMinY(source));
        il.setTileWidth(il.getWidth(source));
        il.setTileHeight(il.getHeight(source));

        return il;
    }

    /**
     * Constructs an <code>UntiledOpImage</code>. The image origin and dimensions, <code>SampleModel</code>, and <code>
     * ColorModel</code> may optionally be specified by an <code>ImageLayout</code> object. In all cases the tile grid
     * offset will be set to the image origin and the tile dimensions to the image dimensions. If not specified in the
     * <code>ImageLayout</code>, the image origin and dimensions are set to the corresponding attributes of the first
     * source image. Cobbling will be performed on the source(s) as needed.
     *
     * @param sources The immediate sources of this image.
     * @param configuration Configurable attributes of the image including configuration variables indexed by <code>
     *     RenderingHints.Key</code>s and image properties indexed by <code>String</code>s or <code>CaselessStringKey
     *     </code>s. This is simply forwarded to the superclass constructor.
     * @param layout an <code>ImageLayout</code> optionally containing the <code>SampleModel</code>, and <code>
     *     ColorModel</code>. The tile grid layout information will be overridden in order to ensure that the image has
     *     a single tile.
     * @throws IllegalArgumentException if <code>sources</code> is <code>null</code>.
     * @throws IllegalArgumentException If <code>sources</code> is non-<code>null</code> and any object in <code>sources
     *     </code> is <code>null</code>.
     * @throws IllegalArgumentException if <code>sources</code> does not contain at least one element.
     * @throws ClassCastException If the first object in <code>sources</code> is not a <code>RenderedImage</code>.
     * @since JAI 1.1
     */
    public UntiledOpImage(Vector sources, Map configuration, ImageLayout layout) {
        super(checkSourceVector(sources, true), layoutHelper(layout, sources), configuration, true);
    }

    /**
     * Constructs an <code>UntiledOpImage</code>. The image origin and dimensions, <code>SampleModel</code>, and <code>
     * ColorModel</code> may optionally be specified by an <code>ImageLayout</code> object. In all cases the tile grid
     * offset will be set to the image origin and the tile dimensions to the image dimensions. If not specified in the
     * <code>ImageLayout</code>, the image origin and dimensions are set to the corresponding attributes of the source
     * image. Cobbling will be performed on the source as needed.
     *
     * @param source a <code>RenderedImage</code>.
     * @param configuration Configurable attributes of the image including configuration variables indexed by <code>
     *     RenderingHints.Key</code>s and image properties indexed by <code>String</code>s or <code>CaselessStringKey
     *     </code>s. This is simply forwarded to the superclass constructor.
     * @param layout an <code>ImageLayout</code> optionally containing the <code>SampleModel</code>, and <code>
     *     ColorModel</code>. The tile grid layout information will be overridden in order to ensure that the image has
     *     a single tile.
     * @throws IllegalArgumentException if <code>source</code> is <code>null</code>.
     * @since JAI 1.1
     */
    public UntiledOpImage(RenderedImage source, Map configuration, ImageLayout layout) {
        super(
                vectorize(source), // vectorize() checks for null source.
                layoutHelper(layout, vectorize(source)),
                configuration,
                true);
    }

    /**
     * Returns the image bounds.
     *
     * @param sourceRect the <code>Rectangle</code> in source coordinates (ignored).
     * @param sourceIndex the index of the source image (ignored).
     * @return The image bounds.
     */
    public Rectangle mapSourceRect(Rectangle sourceRect, int sourceIndex) {
        return getBounds();
    }

    /**
     * Returns the bounds of the indicated source image.
     *
     * @param destRect the Rectangle in destination coordinates (ignored).
     * @param sourceIndex the index of the source image.
     * @return The bounds of the indicated source image.
     * @throws IllegalArgumentException if <code>sourceIndex</code> is negative or greater than the index of the last
     *     source.
     */
    public Rectangle mapDestRect(Rectangle destRect, int sourceIndex) {
        if (sourceIndex < 0 || sourceIndex >= getNumSources()) {
            throw new IllegalArgumentException(JaiI18N.getString("Generic1"));
        }

        return getSource(sourceIndex).getBounds();
    }

    /**
     * Computes a tile. All sources are cobbled together and <code>computeImage</code> is called to produce the single
     * output tile.
     *
     * @param tileX The X index of the tile.
     * @param tileY The Y index of the tile.
     */
    public Raster computeTile(int tileX, int tileY) {
        // Create a new raster.
        Point org = new Point(getMinX(), getMinY());
        WritableRaster dest = createWritableRaster(sampleModel, org);

        // Determine the active area. Since the image has a single
        // tile equal in coverage to the image bounds just set this
        // to the image bounds.
        Rectangle destRect = getBounds();

        // Cobble source image(s).
        int numSources = getNumSources();
        Raster[] rasterSources = new Raster[numSources];
        for (int i = 0; i < numSources; i++) {
            PlanarImage source = getSource(i);
            Rectangle srcRect = mapDestRect(destRect, i);
            rasterSources[i] = source.getData(srcRect);
        }

        // Compute the image.
        computeImage(rasterSources, dest, destRect);

        for (int i = 0; i < numSources; i++) {
            Raster sourceData = rasterSources[i];
            if (sourceData != null) {
                PlanarImage source = getSourceImage(i);

                // Recycle the source tile
                if (source.overlapsMultipleTiles(sourceData.getBounds())) {
                    recycleTile(sourceData);
                }
            }
        }

        return dest;
    }

    /**
     * Calculate the destination image from the source image.
     *
     * @param sources The source Rasters; should be the whole image for each source.
     * @param dest The destination WritableRaster; should be the whole image.
     * @param destRect The destination Rectangle; should equal the destination image bounds.
     * @since JAI 1.1
     */
    protected abstract void computeImage(Raster[] sources, WritableRaster dest, Rectangle destRect);

    /**
     * Returns an array of points indicating the tile dependencies which in this case is the set of all tiles in the
     * specified source image.
     *
     * @since JAI 1.1
     */
    public Point[] getTileDependencies(int tileX, int tileY, int sourceIndex) {
        // Compute the tile dependencies only the first time that this
        // method is invoked.
        PlanarImage source = getSource(sourceIndex);

        int minTileX = source.getMinTileX();
        int minTileY = source.getMinTileY();
        int maxTileX = minTileX + source.getNumXTiles() - 1;
        int maxTileY = minTileY + source.getNumYTiles() - 1;

        Point[] tileDependencies = new Point[(maxTileX - minTileX + 1) * (maxTileY - minTileY + 1)];

        int count = 0;
        for (int ty = minTileY; ty <= maxTileY; ty++) {
            for (int tx = minTileX; tx <= maxTileX; tx++) {
                tileDependencies[count++] = new Point(tx, ty);
            }
        }

        return tileDependencies;
    }
}
